/**
 * Copyright (c) 2018-2023, NXOS Development Team
 * SPDX-License-Identifier: Apache-2.0
 *
 * Contains: 
 *
 * Change Logs:
 * Date           Author            Notes
 * 2023-07-25     Shiroko           Init
 */

#include "nxos.h"

#ifdef CONFIG_NX_DRIVER_VIRTIO_GPU

#undef NX_LOG_MOD_NAME
#define NX_LOG_MOD_NAME "VIRTIO-GPU"

#include "base/log.h"

#define DRV_NAME "virtio gpu"
#define DEV_NAME "virtio_gpu"

#include "../virtio.h"
#include "virtio_gpu.h"
#include <drvfw/framebuffer.h>

NX_PRIVATE NX_Error VirtioGPUSendCommand(
        NX_Device *device, int queue_index,
        void *command, NX_Size command_len, void *result, NX_Size result_len)
{
    NX_ASSERT(result_len >= sizeof(VirtioGPUControlHeader));
    NX_U32 idx[2]; // One for request, one for response.
    VirtioQueue *queue = VirtioQueueGet(device, queue_index);
    while (VirtioQueueDescAllocMany(queue, 2, idx) != NX_EOK);

    VirtioQueueDescFill(queue, idx[0], (NX_U64) ((NX_UArch) NX_Virt2Phy(command)), command_len,
                        VIRTIO_QUEUE_DESC_FLAG_NEXT, idx[1]);
    VirtioQueueDescFill(queue, idx[1], (NX_U64) ((NX_UArch) NX_Virt2Phy(result)), result_len,
                        VIRTIO_QUEUE_DESC_FLAG_WRITE, 0);

    VirtioQueuePutIntoAvailRing(queue, idx[0]);
    VirtioSendNotification(device, queue_index, idx[0]);
    VirtioWaitNotification(device, queue_index, idx[0]);
    VirtioQueueDescFreeMany(queue, idx[0]);

    return NX_EOK; // This OK indicates successful of command sending, not the response ok
}

NX_PRIVATE NX_Error VirtioGPUSendCursorCommand(
        NX_Device *device, int queue_index,
        void *command, NX_Size command_len)
{
    // In specification, Cursor command DOES HAVE A RESPONSE.
    // In Qemu implementation, Cursor command DOES NOT HAVE A RESPONSE.
    NX_U32 idx;
    VirtioQueue *queue = VirtioQueueGet(device, queue_index);
    while ((idx = VirtioQueueDescAlloc(queue)) == VIRTIO_DEVICE_ID_INVALID);

    VirtioQueueDescFill(queue, idx, (NX_U64) ((NX_UArch) NX_Virt2Phy(command)), command_len, 0, 0);

    VirtioQueuePutIntoAvailRing(queue, idx);
    VirtioSendNotification(device, queue_index, idx);
    VirtioWaitNotification(device, queue_index, idx);
    VirtioQueueDescFree(queue, idx);

    return NX_EOK;
}

NX_PRIVATE NX_U32 VirtioGPUAllocResourceId(NX_Device *device)
{
    VirtioGPUDeviceExtension *deviceExtension = VirtioGetDevExtension(device, VirtioGPUDeviceExtension);
    return (deviceExtension->availableResourceId++);
}

/*
 * Control commands - Info
 * IMPROVE: Those with extra response could be improved via chaining 3 descriptor
 */

NX_PRIVATE NX_Error VirtioGPUCommandGetDisplayInfo(NX_Device *device, VirtioGPUDisplayInfo *displayInfo)
{
    VirtioGPUControlHeader req = {
            .type = VIRTIO_GPU_CMD_GET_DISPLAY_INFO
    };
    VirtioGPUResponseDisplayInfo resp;
    VirtioGPUSendCommand(device, VIRTIO_GPU_QUEUE_CONTROL,
                         &req, sizeof(req),
                         &resp, sizeof(resp));
    if (resp.header.type != VIRTIO_GPU_RESP_OK_DISPLAY_INFO)
        return NX_EFAULT;
    NX_MemCopy(displayInfo, &resp.displayInfo, sizeof(VirtioGPUDisplayInfo));
    return NX_EOK;
}

NX_PRIVATE NX_Size VirtioGPUCommandGetEDID(NX_Device *device, NX_U32 scanoutId, NX_U8 *edid, NX_Size size)
{
    VirtioGPURequestEDID req = {
            .header = {
                    .type = VIRTIO_GPU_CMD_GET_EDID,
            },
            .scanout = scanoutId
    };
    VirtioGPUResponseEDID resp;
    VirtioGPUSendCommand(device, VIRTIO_GPU_QUEUE_CONTROL,
                         &req, sizeof(req),
                         &resp, sizeof(resp));
    if (resp.header.type != VIRTIO_GPU_RESP_OK_EDID)
        return -1;
    if (resp.size > size)
        return -1;
    NX_MemCopy(edid, &resp.edid, resp.size);
    return resp.size;
}

NX_PRIVATE NX_Error
VirtioGPUCommandGetCapsetInfo(NX_Device *device, NX_U32 capsetId, VirtioGPUCapsetInfo *capsetInfo)
{
    VirtioGPURequestGetCapsetInfo req = {
            .header = {
                    .type = VIRTIO_GPU_CMD_GET_CAPSET_INFO
            },
            .capset_index =  capsetId
    };
    VirtioGPUResponseCapsetInfo resp;
    VirtioGPUSendCommand(device, VIRTIO_GPU_QUEUE_CONTROL,
                         &req, sizeof(req),
                         &resp, sizeof(resp));
    if (resp.header.type != VIRTIO_GPU_RESP_OK_CAPSET_INFO)
        return NX_EFAULT;
    NX_MemCopy(capsetInfo, &resp.capsetInfo, sizeof(VirtioGPUCapsetInfo));
    return NX_EOK;
}

NX_PRIVATE NX_Error VirtioGPUCommandGetCapset(
        NX_Device *device, NX_U32 capsetId, NX_U32 capsetVer, NX_U8 *capsetData, NX_Size capsetDataSize)
{
    VirtioGPURequestGetCapset req = {
            .header = {
                    .type = VIRTIO_GPU_CMD_GET_CAPSET
            },
            .capset_id = capsetId,
            .capset_version = capsetVer
    };
    VirtioGPUResponseCapset *resp = NX_MemAlloc(sizeof(VirtioGPUResponseCapset) + capsetDataSize);
    if (resp == NX_NULL) return NX_ENOMEM;
    VirtioGPUSendCommand(device, VIRTIO_GPU_QUEUE_CONTROL,
                         &req, sizeof(req),
                         resp, sizeof(VirtioGPUResponseCapset) + capsetDataSize);
    if (resp->header.type != VIRTIO_GPU_RESP_OK_CAPSET)
        return NX_EFAULT;
    NX_MemCopy(capsetData, &resp->capset_data, capsetDataSize);
    return NX_EOK;
}

/*
 * Control commands - Setup
 */

NX_PRIVATE NX_Error
VirtioGPUCommandSetScanout(NX_Device *device, NX_U32 scanoutId, NX_U32 resourceId, VirtioGPURect rect)
{
    VirtioGPUDeviceExtension *deviceExtension = VirtioGetDevExtension(device, VirtioGPUDeviceExtension);
    if (resourceId == 0 || scanoutId >= VIRTIO_GPU_MAX_SCANOUTS ||
        scanoutId >= deviceExtension->deviceConfig->num_scanouts ||
        deviceExtension->scanouts[scanoutId].enabled != NX_True)
        return NX_EINVAL;
    VirtioGPUControlHeader resp;
    VirtioGPURequestSetScanout req = {
            .header = {
                    .type = VIRTIO_GPU_CMD_SET_SCANOUT
            },
            .scanout_id = scanoutId,
            .resource_id = resourceId,
            .rect = rect
    };

    VirtioGPUSendCommand(device, VIRTIO_GPU_QUEUE_CONTROL,
                         &req, sizeof(req),
                         &resp, sizeof(resp));
    if (resp.type != VIRTIO_GPU_RESP_OK_NODATA)
        return NX_EFAULT;
    return NX_EOK;
}

/*
 * Control commands - Resource
 */

NX_PRIVATE NX_U32
VirtioGPUCommandResourceCreate2D(NX_Device *device, NX_U32 height, NX_U32 width, VirtioGPUFormats format)
{
    NX_U32 resId = VirtioGPUAllocResourceId(device);
    if (resId == 0) return 0;
    VirtioGPUControlHeader resp;
    VirtioGPURequestCreate2D req = {
            .header = {
                    .type = VIRTIO_GPU_CMD_RESOURCE_CREATE_2D
            },
            .resource_id = resId,
            .format = format,
            .width = width,
            .height = height
    };

    VirtioGPUSendCommand(device, VIRTIO_GPU_QUEUE_CONTROL,
                         &req, sizeof(req),
                         &resp, sizeof(resp));
    if (resp.type != VIRTIO_GPU_RESP_OK_NODATA)
        return 0;
    return resId;
}

NX_PRIVATE NX_Error VirtioGPUCommandResourceUnref(NX_Device *device, NX_U32 resourceId)
{
    if (resourceId == 0) return NX_EINVAL;
    VirtioGPUControlHeader resp;
    VirtioGPURequestUnref req = {
            .header = {
                    .type = VIRTIO_GPU_CMD_RESOURCE_UNREF
            },
            .resource_id = resourceId,
    };

    VirtioGPUSendCommand(device, VIRTIO_GPU_QUEUE_CONTROL,
                         &req, sizeof(req),
                         &resp, sizeof(resp));
    if (resp.type != VIRTIO_GPU_RESP_OK_NODATA)
        return NX_EFAULT;
    return NX_EOK;
}

NX_PRIVATE NX_Error VirtioGPUCommandResourceAttachBacking(
        NX_Device *device,
        NX_U32 resourceId,
        NX_U32 entries_count,
        VirtioGPURequestResourceAttachBackingEntry *entries)
{
    if (resourceId == 0 || entries_count == 0 || entries == NX_NULL) return NX_EINVAL;
    VirtioGPUControlHeader resp;
    NX_Size reqAttachSize = sizeof(VirtioGPURequestResourceAttachBacking) +
                            sizeof(VirtioGPURequestResourceAttachBackingEntry) *
                            entries_count;

    VirtioGPURequestResourceAttachBacking *req = NX_MemAlloc(reqAttachSize);
    if (req == NX_NULL) return NX_ENOMEM;
    req->header.type = VIRTIO_GPU_CMD_RESOURCE_ATTACH_BACKING;
    req->resource_id = resourceId;
    req->nr_entries = entries_count;
    NX_MemCopy(req->entries, entries, sizeof(VirtioGPURequestResourceAttachBackingEntry) * entries_count);
    VirtioGPUSendCommand(device, VIRTIO_GPU_QUEUE_CONTROL,
                         req, reqAttachSize,
                         &resp, sizeof(resp));
    if (resp.type != VIRTIO_GPU_RESP_OK_NODATA)
        return NX_EFAULT;
    return NX_EOK;
}

NX_PRIVATE NX_Error VirtioGPUCommandResourceDetachBacking(NX_Device *device, NX_U32 resourceId)
{
    if (resourceId == 0) return NX_EINVAL;
    VirtioGPUControlHeader resp;
    VirtioGPURequestSetScanout req = {
            .header = {
                    .type = VIRTIO_GPU_CMD_RESOURCE_DETACH_BACKING
            },
            .resource_id = resourceId,
    };

    VirtioGPUSendCommand(device, VIRTIO_GPU_QUEUE_CONTROL,
                         &req, sizeof(req),
                         &resp, sizeof(resp));
    if (resp.type != VIRTIO_GPU_RESP_OK_NODATA)
        return NX_EFAULT;
    return NX_EOK;
}

NX_PRIVATE NX_Error VirtioGPUCommandResourceFlush(NX_Device *device, NX_U32 resourceId, VirtioGPURect rect)
{
    if (resourceId == 0) return NX_EINVAL;
    VirtioGPUControlHeader resp;
    VirtioGPURequestResourceFlush req = {
            .header = {
                    .type = VIRTIO_GPU_CMD_RESOURCE_FLUSH
            },
            .resource_id = resourceId,
    };
    NX_MemCopy(&req.rect, &rect, sizeof(rect));

    VirtioGPUSendCommand(device, VIRTIO_GPU_QUEUE_CONTROL,
                         &req, sizeof(req),
                         &resp, sizeof(resp));
    if (resp.type != VIRTIO_GPU_RESP_OK_NODATA)
        return NX_EFAULT;
    return NX_EOK;
}

NX_PRIVATE NX_Error
VirtioGPUCommandResourceTransferToHost2D(NX_Device *device, NX_U32 resourceId, VirtioGPURect rect, NX_U64 offset)
{
    // rect x&y is target host x&y
    if (resourceId == 0) return NX_EINVAL;
    VirtioGPUControlHeader resp;
    VirtioGPURequestTransferToHost2D req = {
            .header = {
                    .type = VIRTIO_GPU_CMD_TRANSFER_TO_HOST_2D
            },
            .resource_id = resourceId,
            .offset = offset
    };
    NX_MemCopy(&req.rect, &rect, sizeof(rect));

    VirtioGPUSendCommand(device, VIRTIO_GPU_QUEUE_CONTROL,
                         &req, sizeof(req),
                         &resp, sizeof(resp));
    if (resp.type != VIRTIO_GPU_RESP_OK_NODATA)
        return NX_EFAULT;
    return NX_EOK;
}

#if 0
NX_PRIVATE NX_Error VirtioGPUCommandResourceAssignUUID(NX_Device *device, NX_U32 resourceId, NX_U8 UUID[16])
{
    if (resourceId == 0 || UUID == NX_NULL) return NX_EINVAL;
    VirtioGPURequestResourceAssignUUID req = {
            .header = {
                    .type = VIRTIO_GPU_CMD_RESOURCE_ASSIGN_UUID
            },
            .resource_id = resourceId,
    };
    VirtioGPUResponseResourceUUID resp;

    VirtioGPUSendCommand(device, VIRTIO_GPU_QUEUE_CONTROL,
                         &req, sizeof(req),
                         &resp, sizeof(resp));
    if (resp.header.type != VIRTIO_GPU_RESP_OK_RESOURCE_UUID)
        return NX_EFAULT;
    NX_MemCopy(UUID, &resp.uuid, sizeof(NX_U8) * 16);
    return NX_EOK;
}
#endif

// IMPROVE: implement BLOB things and 3D part

/*
 * Cursor command
 */

// HotX/HotY is hot-point of cursor
NX_PRIVATE NX_Error VirtioGPUCommandCursorUpdate(
        NX_Device *device,
        NX_U32 x, NX_U32 y, NX_U32 resourceId, NX_U32 scanoutId,
        NX_U32 hotX, NX_U32 hotY)
{
    VirtioGPUDeviceExtension *deviceExtension = VirtioGetDevExtension(device, VirtioGPUDeviceExtension);
    if (resourceId == 0 || scanoutId >= VIRTIO_GPU_MAX_SCANOUTS ||
        scanoutId >= deviceExtension->deviceConfig->num_scanouts ||
        deviceExtension->scanouts[scanoutId].enabled != NX_True)
        return NX_EINVAL;
    VirtioGPURequestUpdateCursor req = {
            .header = {
                    .type = VIRTIO_GPU_CMD_UPDATE_CURSOR
            },
            .resource_id = resourceId,
            .hot_x = hotX,
            .hot_y = hotY,
            .pos = {
                    .x = x,
                    .y = y,
                    .scanout_id = scanoutId
            }
    };

    VirtioGPUSendCursorCommand(device, VIRTIO_GPU_QUEUE_CURSOR,
                               &req, sizeof(req));
    return NX_EOK;
}

NX_PRIVATE NX_Error VirtioGPUCommandCursorMove(
        NX_Device *device,
        NX_U32 x, NX_U32 y, NX_U32 scanoutId)
{
    VirtioGPUDeviceExtension *deviceExtension = VirtioGetDevExtension(device, VirtioGPUDeviceExtension);
    if (scanoutId >= VIRTIO_GPU_MAX_SCANOUTS ||
        scanoutId >= deviceExtension->deviceConfig->num_scanouts ||
        deviceExtension->scanouts[scanoutId].enabled != NX_True)
        return NX_EINVAL;
    VirtioGPURequestUpdateCursor req = {
            .header = {
                    .type = VIRTIO_GPU_CMD_MOVE_CURSOR
            },
            .pos = {
                    .x = x,
                    .y = y,
                    .scanout_id = scanoutId
            }
    };

    VirtioGPUSendCursorCommand(device, VIRTIO_GPU_QUEUE_CURSOR,
                               &req, sizeof(req));
    return NX_EOK;
}

NX_PRIVATE NX_Error VirtioGPUFreeResource(NX_Device *device, NX_U32 resourceId, void *buffer)
{
    if (resourceId == 0) return NX_EINVAL;
    if (VirtioGPUCommandResourceDetachBacking(device, resourceId) != NX_EOK)
        return NX_EFAULT;
    if (VirtioGPUCommandResourceUnref(device, resourceId) != NX_EOK)
        return NX_EFAULT;
    NX_PageFree(buffer);
    return NX_EOK;
}

NX_PRIVATE NX_Error VirtioGPUCreateResource(
        NX_Device *device, VirtioGPURect rect, VirtioGPUFormats format,
        NX_U32 *out_resourceId, void **out_buffer, NX_Size *out_size)
{
    NX_Size framebufferValidSize = 4 * rect.height * rect.width;
    NX_Size framebufferSize = NX_PAGE_ALIGNUP(framebufferValidSize);
    void *framebuffer = NX_PageAlloc(framebufferSize / NX_PAGE_SIZE);
    if (framebuffer == NX_NULL) return NX_ENOMEM;

    // Setup framebuffer
    NX_U32 resourceId = VirtioGPUCommandResourceCreate2D(device, rect.height, rect.width, format);
    if (resourceId == 0) goto failed;
    VirtioGPURequestResourceAttachBackingEntry entries[1] = {
            {
                    .addr = (NX_U64) ((NX_UArch) NX_Virt2Phy(framebuffer)),
                    .length = framebufferValidSize
            }
    };
    if (VirtioGPUCommandResourceAttachBacking(device, resourceId, 1, entries) != NX_EOK)
        goto failed;
    *out_resourceId = resourceId;
    *out_buffer = framebuffer;
    *out_size = framebufferValidSize;
    return NX_EOK;
failed:
    if (resourceId != 0)
        VirtioGPUCommandResourceUnref(device, resourceId);
    NX_PageFree(framebuffer);
    return NX_EFAULT;
}

/*
 * Virtio GPU Initialize
 */
NX_PRIVATE NX_Error VirtioGPUScanoutFree(NX_Device *device, VirtioGPUScanout *scanout)
{
    VirtioGPUCommandSetScanout(device, scanout->id, 0, scanout->rect); // disable scanout
    VirtioGPUFreeResource(device, scanout->resourceId, scanout->framebuffer);
    scanout->cursorFramebuffer = NX_NULL;
    scanout->cursorResourceId = 0;
    return NX_EOK;
}

NX_PRIVATE NX_Error
VirtioGPUScanoutSetup(NX_Device *device, VirtioGPUScanout *scanout, VirtioGPURect rect, VirtioGPUFormats format)
{
    NX_LOG_D("Setup scanout %d using %dx%d", scanout->id, rect.width, rect.height);
    // If already initialized, free it
    if (scanout->framebuffer != NX_NULL)
        VirtioGPUScanoutFree(device, scanout);

    NX_MemCopy(&scanout->rect, &rect, sizeof(VirtioGPURect));
    // Virtio-GPU support page-flip(ping-pong buffer/double buffer), But I couldn't find out the necessity.
    // Create framebuffer resource
    if (VirtioGPUCreateResource(
            device, rect, format,
            &scanout->resourceId, &scanout->framebuffer, &scanout->framebufferValidSize)
        != NX_EOK)
        return NX_EFAULT;

    if (VirtioGPUCommandSetScanout(device, scanout->id, scanout->resourceId, scanout->rect) != NX_EOK)
    {
        VirtioGPUFreeResource(device, scanout->resourceId, scanout->framebuffer);
        scanout->resourceId = 0;
        scanout->framebuffer = NX_NULL;
        return NX_EFAULT;
    }
    scanout->format = format;
    return NX_EOK;
}


NX_PRIVATE void VirtioGPUCleanUp(NX_Device *device);

NX_PRIVATE NX_Error VirtioGPUInit(NX_Driver *driver, NX_Device *device)
{
    // IMPROVE: break major device into scanout specified minor device
    NX_Error err = NX_EOK;
    NX_LOG_D("VirtIO GPU device initialize for %s.", device->name);
    NX_U32 features = 0;
    features &= ~(VIRTIO_GPU_FEATURE_VIRGL | VIRTIO_GPU_FEATURE_RESOURCE_UUID |
                  VIRTIO_GPU_FEATURE_RESOURCE_BLOB | VIRTIO_GPU_FEATURE_CONTEXT_INIT);
    features &= VIRTIO_GPU_FEATURE_EDID;

    // Negotiate features & status
    if ((err = VirtioDeviceInitialize(device, &features, 1)) != NX_EOK)
        return err;

    // Device-specified initialization
    VirtioQueueAlloc(device, 2);
    VirtioQueueInit(device, VIRTIO_GPU_QUEUE_CONTROL, CONFIG_NX_DRIVER_VIRTIO_SPLIT_QUEUE_SIZE);
    VirtioQueueInit(device, VIRTIO_GPU_QUEUE_CURSOR, CONFIG_NX_DRIVER_VIRTIO_SPLIT_QUEUE_SIZE);

    // Driver OK
    VirtioDeviceDriverOK(device);

    VirtioGPUDeviceExtension *deviceExtension = NX_MemAlloc(sizeof(VirtioGPUDeviceExtension));
    if (deviceExtension == NX_NULL)
        goto nomem_failed;
    VirtioGetExtension(device)->devExt = (void *) deviceExtension;

    VirtioGPUDeviceConfig *deviceCfg = NX_MemAlloc(sizeof(VirtioGPUDeviceConfig));
    if (deviceCfg == NX_NULL)
        goto nomem_failed;
    VirtioReadDeviceConfig(device, 0, deviceCfg, sizeof(VirtioGPUDeviceConfig));

    deviceExtension->deviceConfig = deviceCfg;
    deviceExtension->availableResourceId = 1;

    // Query display info
    VirtioGPUDisplayInfo displayInfo;
    if ((err = VirtioGPUCommandGetDisplayInfo(device, &displayInfo)) != NX_EOK)
        goto failed;

    for (int i = 0; i < deviceCfg->num_scanouts; i++)
        if (displayInfo.modes[i].enabled)
        {
            VirtioGPURect *rect = &displayInfo.modes[i].rect;
            NX_LOG_D("Initialize scanout %d, preferred mode: %dx%d@(%d, %d)", i, rect->width, rect->height, rect->x,
                     rect->y);
            VirtioGPUScanout *scanout = &deviceExtension->scanouts[i];
            scanout->enabled = NX_True;
            scanout->id = i;
            scanout->framebuffer = NX_NULL;
            scanout->cursorFramebuffer = NX_NULL;
            if (VirtioGPUScanoutSetup(device, scanout, *rect, VIRTIO_GPU_FORMAT_B8G8R8A8_UNORM) != NX_EOK)
            {
                scanout->enabled = NX_False;
                NX_LOG_D("Scanout %d initialize failed!", i);
            }
        }

    return NX_EOK;
nomem_failed:
    err = NX_ENOMEM;
failed:
    VirtioGPUCleanUp(device);
    return err;
}

NX_PRIVATE void VirtioGPUCleanUp(NX_Device *device)
{
    // Reset the device
    VirtioDeviceReset(device);
    // Cleanup VirtioQueue
    for (int i = 0; i < VirtioGetExtension(device)->queue_num; i++)
        VirtioQueueDestroy(device, i);
    VirtioQueueFree(device);

    VirtioGPUDeviceExtension *deviceExtension = VirtioGetDevExtension(device, VirtioGPUDeviceExtension);
    if (deviceExtension == NX_NULL) return;
    if (deviceExtension->deviceConfig != NX_NULL)
    {
        NX_MemFree(deviceExtension->deviceConfig);
    }
    NX_MemFree(deviceExtension);
}

NX_PRIVATE NX_Error open(struct NX_Device *device, NX_U32 flags)
{
    VirtioGPUDeviceExtension *deviceExtension = VirtioGetDevExtension(device, VirtioGPUDeviceExtension);
    for (int i = 0; i < deviceExtension->deviceConfig->num_scanouts; i++)
        if (deviceExtension->scanouts[i].enabled == NX_True)
            deviceExtension->selectedScanout = i;
    return NX_EOK;
}

NX_PRIVATE NX_Error close(struct NX_Device *device)
{
    return NX_EOK; // Do nothing
}

NX_PRIVATE NX_Error read(struct NX_Device *device, void *buf, NX_Offset off, NX_Size len, NX_Size *outLen)
{
    VirtioGPUDeviceExtension *deviceExtension = VirtioGetDevExtension(device, VirtioGPUDeviceExtension);
    VirtioGPUScanout *scanout = &deviceExtension->scanouts[deviceExtension->selectedScanout];
    if (scanout->enabled != NX_True) return NX_ENORES;
    if (off + len > scanout->framebufferValidSize) return NX_EINVAL;
    NX_CopyToUser(buf, scanout->framebuffer + off, len);
    *outLen = len;
    return NX_EOK;
}

NX_PRIVATE NX_Error write(struct NX_Device *device, void *buf, NX_Offset off, NX_Size len, NX_Size *outLen)
{
    // Directly write into framebuffer
    VirtioGPUDeviceExtension *deviceExtension = VirtioGetDevExtension(device, VirtioGPUDeviceExtension);
    VirtioGPUScanout *scanout = &deviceExtension->scanouts[deviceExtension->selectedScanout];
    if (scanout->enabled != NX_True) return NX_ENORES;
    if (off + len > scanout->framebufferValidSize) return NX_EINVAL;
    NX_CopyFromUser(scanout->framebuffer + off, buf, len);
    *outLen = len;
    return NX_EOK;
}

NX_PRIVATE NX_Error control(struct NX_Device *device, NX_U32 cmd, void *arg)
{
    VirtioGPUDeviceExtension *deviceExtension = VirtioGetDevExtension(device, VirtioGPUDeviceExtension);
    int scanoutId = deviceExtension->selectedScanout;
    arg = VirtioVirt2Phy(arg);
    VirtioGPUControlArgument *ctrlArg = (VirtioGPUControlArgument *) arg;
    if (arg == NX_NULL
        && cmd != NX_FRAMEBUFFER_CMD_GETINFO && cmd != NX_FRAMEBUFFER_CMD_PRESENT &&
        cmd != VIRTIO_GPU_CMD_FRAMEBUFFER_TRANSFER &&
        cmd != VIRTIO_GPU_CMD_FRAMEBUFFER_FLUSH)
        return NX_EINVAL;
    if ((arg == NX_NULL || ctrlArg->data == NX_NULL)
        && cmd != NX_FRAMEBUFFER_CMD_GETINFO && cmd != NX_FRAMEBUFFER_CMD_PRESENT &&
        cmd != VIRTIO_GPU_CMD_FRAMEBUFFER_TRANSFER &&
        cmd != VIRTIO_GPU_CMD_FRAMEBUFFER_FLUSH && cmd != VIRTIO_GPU_CMD_SETUP_SCANOUT &&
        cmd != VIRTIO_GPU_CMD_CURSOR_MOVE)
        return NX_EINVAL;
    if (ctrlArg != NX_NULL && cmd != NX_FRAMEBUFFER_CMD_GETINFO && cmd != NX_FRAMEBUFFER_CMD_PRESENT &&
        cmd != VIRTIO_GPU_CMD_QUERY_CAPSET_INFO &&
        cmd != VIRTIO_GPU_CMD_QUERY_CAPSET)
        scanoutId = ctrlArg->scanoutId;

    if (deviceExtension->scanouts[scanoutId].enabled == NX_False)
        return NX_ENORES;
    VirtioGPUScanout *scanout = &deviceExtension->scanouts[scanoutId];

    switch (cmd)
    {
    case NX_FRAMEBUFFER_CMD_GETINFO:
    {
        NX_FramebufferInfo *info = (NX_FramebufferInfo *) arg;
        if (info)
        {
            info->bitsPerPixel = 32;
            info->bytesPerScanLine = 4 * scanout->rect.width;
            info->xResolution = scanout->rect.width;
            info->yResolution = scanout->rect.height;
            info->phyBasePtr = (NX_Addr) scanout->framebuffer;
        }
        break;
    }
    case NX_FRAMEBUFFER_CMD_PRESENT:
        if (arg == NX_NULL)
        {
            // Update all
            VirtioGPUCommandResourceTransferToHost2D(device, scanout->resourceId, scanout->rect, 0);
            VirtioGPUCommandResourceFlush(device, scanout->resourceId, scanout->rect);
        }
        else
        {
            // arg is NX_Region in sdk/wm/surface.h
            struct
            {
                int x, y, w, h;
            } *rect = VirtioVirt2Phy(arg);
            if(rect == NX_NULL)
                return NX_EINVAL;
            /*
            VirtioGPURect rect_g = {
                    .x = rect->x,
                    .y = rect->y,
                    .width = rect->w,
                    .height = rect->h
            };
            */
            VirtioGPUCommandResourceTransferToHost2D(device, scanout->resourceId, scanout->rect, 0);
            VirtioGPUCommandResourceFlush(device, scanout->resourceId, scanout->rect);
        }
        break;
    case VIRTIO_GPU_CMD_SELECT_SCANOUT:
        deviceExtension->selectedScanout = scanoutId;
        break;
    case VIRTIO_GPU_CMD_QUERY_DISPLAY_INFO:
        if (ctrlArg->dataSize < sizeof(VirtioGPUDisplayInfo)) return NX_EINVAL;
        return VirtioGPUCommandGetDisplayInfo(device, ctrlArg->data);
    case VIRTIO_GPU_CMD_QUERY_EDID:
        return VirtioGPUCommandGetEDID(device, scanoutId, ctrlArg->data, ctrlArg->dataSize);
    case VIRTIO_GPU_CMD_QUERY_CAPSET_INFO:
        if (ctrlArg->dataSize < sizeof(VirtioGPUCapsetInfo)) return NX_EINVAL;
        return VirtioGPUCommandGetCapsetInfo(device, ctrlArg->arg.queryCapsetInfo.capsetId, ctrlArg->data);
    case VIRTIO_GPU_CMD_QUERY_CAPSET:
        return VirtioGPUCommandGetCapset(device, ctrlArg->arg.queryCapset.capsetId, ctrlArg->arg.queryCapset.capsetVer,
                                         ctrlArg->data, ctrlArg->dataSize);
    case VIRTIO_GPU_CMD_FRAMEBUFFER_TRANSFER:
        if (ctrlArg == NX_NULL)
        {
            // Transfer all framebuffer to host
            return VirtioGPUCommandResourceTransferToHost2D(device, scanout->resourceId, scanout->rect, 0);
        }
        else
        {
            return VirtioGPUCommandResourceTransferToHost2D(device, scanout->resourceId,
                                                            ctrlArg->rect, ctrlArg->arg.fb_offset);
        }
    case VIRTIO_GPU_CMD_FRAMEBUFFER_FLUSH:
        if (ctrlArg == NX_NULL)
        {
            // Flush all framebuffer
            return VirtioGPUCommandResourceFlush(device, scanout->resourceId, scanout->rect);
        }
        else
        {
            return VirtioGPUCommandResourceFlush(device, scanout->resourceId, ctrlArg->rect);
        }
    case VIRTIO_GPU_CMD_CURSOR_UPDATE:
        if (ctrlArg->dataSize != 64 * 64 * 4) return NX_EINVAL; // Must provide 64x64 bitmap
        if (scanout->cursorResourceId == 0)
        {
            // No cursor resource yet, create one.
            NX_U32 cursorResourceId;
            void *cursorFramebuffer;
            NX_Size cursorFramebufferSize;
            VirtioGPURect cursorRect = {
                    .x = 0,
                    .y = 0,
                    .width = 64,
                    .height = 64
            };
            NX_Error err = NX_EOK;
            if ((err = VirtioGPUCreateResource(
                    device, cursorRect, ctrlArg->format,
                    &cursorResourceId, &cursorFramebuffer, &cursorFramebufferSize))
                != NX_EOK)
                return err;
            NX_MemCopy(cursorFramebuffer, ctrlArg->data, ctrlArg->dataSize);
            if (VirtioGPUCommandResourceTransferToHost2D(
                    device, cursorResourceId, ctrlArg->rect,
                    ctrlArg->arg.cursorUpdate.fb_offset) != NX_EOK ||
                VirtioGPUCommandResourceFlush(device, cursorResourceId, ctrlArg->rect) != NX_EOK ||
                VirtioGPUCommandCursorUpdate(device, ctrlArg->arg.cursorUpdate.x, ctrlArg->arg.cursorUpdate.y,
                                             cursorResourceId, scanoutId, ctrlArg->arg.cursorUpdate.hotX,
                                             ctrlArg->arg.cursorUpdate.hotY) != NX_EOK)
            {
                VirtioGPUFreeResource(device, cursorResourceId, cursorFramebuffer);
                return NX_EFAULT;
            }
            scanout->cursorFramebuffer = cursorFramebuffer;
            scanout->cursorResourceId = cursorResourceId;
        }
        else
        {
            // Already create resource
            NX_MemCopy(scanout->cursorFramebuffer, ctrlArg->data, ctrlArg->dataSize);
            if (VirtioGPUCommandResourceTransferToHost2D(device, scanout->cursorResourceId, ctrlArg->rect,
                                                         ctrlArg->arg.cursorUpdate.fb_offset) != NX_EOK)
                return NX_EFAULT;
            if (VirtioGPUCommandResourceFlush(device, scanout->cursorResourceId, ctrlArg->rect) != NX_EOK)
                return NX_EFAULT;
        }
        break;
    case VIRTIO_GPU_CMD_CURSOR_MOVE:
        return VirtioGPUCommandCursorMove(device, ctrlArg->arg.cursorMove.x, ctrlArg->arg.cursorMove.y, scanoutId);
    case VIRTIO_GPU_CMD_SETUP_SCANOUT:
        return VirtioGPUScanoutSetup(device, scanout, ctrlArg->rect, ctrlArg->format);
    default:
        return NX_EINVAL;
    }

    return NX_EOK;
}

NX_PRIVATE NX_Error mappable(struct NX_Device *device, NX_Size length, NX_U32 port, NX_Addr *outPhyAddr)
{
    VirtioGPUDeviceExtension *deviceExtension = VirtioGetDevExtension(device, VirtioGPUDeviceExtension);
    NX_U32 scanoutId = deviceExtension->selectedScanout;
    if (deviceExtension->scanouts[scanoutId].enabled != NX_True) return NX_ENORES;
    if (length > deviceExtension->scanouts[scanoutId].framebufferValidSize) return NX_EPERM;
    *outPhyAddr = (NX_Addr) deviceExtension->scanouts[scanoutId].framebuffer;
    return NX_EOK;
}

NX_PRIVATE NX_DriverOps virtioGPUDriverOps = {
        .open = open,
        .close = close,
        .read = read,
        .write = write,
        .control = control,
        .mappable = mappable
};


NX_PRIVATE VirtioDriverTableEntity virtioGPUDriverTableEntity = {
        .deviceId = VIRTIO_DEVICE_ID_GPU,
        .driverName = "virtio-gpu",
        .deviceNameTemplate = "virtio_gpu_%d",
        .deviceType = NX_DEVICE_TYPE_SCREEN,
        .driverOps = &virtioGPUDriverOps,
        .initHandler = VirtioGPUInit,
        .cleanUpHandler = VirtioGPUCleanUp
};

VirtioDriverTableEntity *VirtioDriverTableEntityForVirtioGPU(void)
{
    return &virtioGPUDriverTableEntity;
}

#else

#include "../virtio.h"

VirtioDriverTableEntity *VirtioDriverTableEntityForVirtioGPU(void)
{
    return NX_NULL;
}

#endif // CONFIG_NX_DRIVER_VIRTIO_GPU
